package com.google.code.yargon.ui;

import java.io.IOException;
import java.util.HashMap;

import org.apache.log4j.Logger;

import com.google.code.yargon.*;
import com.google.code.yargon.level.LevelManager;
import com.google.code.yargon.ui.component.*;
import com.google.code.yargon.ui.lighting.Ray;
import com.google.code.yargon.ui.lighting.RayCast;
import com.google.code.yargon.util.*;

import org.lwjgl.LWJGLException;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.newdawn.slick.*;
import org.newdawn.slick.opengl.*;
import org.newdawn.slick.font.effects.ColorEffect;
import org.newdawn.slick.util.ResourceLoader;

import static org.lwjgl.opengl.GL11.*;

import static com.google.code.yargon.util.Constants.*;


/**
 * Render engine
 * 
 * @author Mika Myllynen
 *
 */
public class RenderEngine {
		
	private PropertyUtil props = PropertyUtil.getInstance();
	private static Logger log = LogUtil.getInstance();
	
	// Textures: add any new textures here and in loadTextures() as well
	public static final String TEXTURE_UI_BACKGROUND = "gfx/ui/background.png";
	
	// Fonts: add any new fonts here and in loadFonts() as well. String is: fontpath:size
	public static final String FONT_DEFAULT = "gfx/fonts/VeraMono.ttf:12";
	
	// Texture & fonts storage
	private static HashMap<String, Texture> textureMap;
	private static HashMap<String, UnicodeFont> fontMap;
	
	// Light maps
	private static HashMap<Integer, Ray[]> lightMaps = new HashMap<Integer, Ray[]>();
	
	// Screen components
	private ComponentManager cManager;
	private LevelComponent levelComponent;
	private MessageComponent messageComponent;
	private DummyComponent messageComponentBg;
	
	// 0 = use quad strips when rendering multiple quads, 1 = use vertex arrays, 2 = use vertex buffer objects 
	private static int renderMode = 2;
	
	// Singleton RenderEngine
	private static RenderEngine instance = null;
				
	/**
	 * Private constructor, this is a singleton class
	 * 
	 */
	private RenderEngine() {
		textureMap = new HashMap<String, Texture>();
		fontMap = new HashMap<String, UnicodeFont>();	
		
		// Create window
		try {
			Display.setDisplayMode(new DisplayMode(Yargon.getInstance().getWindowWidth(), Yargon.getInstance().getWindowHeight()));
			Display.setTitle("Yargon");
			Display.create();
		} catch(LWJGLException e) {
			log.fatal("Could not initialize display: "+e);
			System.exit(1);
		}
		
		// Get and set render mode
		try {
			renderMode = props.getInt("yargon.opengl.rendermode");
		} catch(Exception e) {
			renderMode = 0;
			log.warn("Error setting render mode, falling back to default");
		}
		if(renderMode != 0 && renderMode != 1) {
			renderMode = 0;
			log.warn("Unknown render mode, falling back to default");
		}
		
		// Initialize (reset to defaults) OpenGL
		resetView();	
	}
	
	public void initFirstPhase() {
		
		loadTextures();										
		loadFonts();	
							
		// Create message component and it's background
		int w = Yargon.getInstance().getWindowWidth() - Yargon.getInstance().getUIWidth();
		int h = 70;
		messageComponentBg = new DummyComponent(0, 0, w, h, 0.7f);	
		messageComponent = new MessageComponent(0, 0, w, h, 1.0f);	
		messageComponent.addHotspot("pinned", w - 12, h - 12, 12, 12);
		messageComponent.addHotspot("scrollup", w - 12, h - 24, 12, 12);
		messageComponent.addHotspot("scrolldown", w - 12, 0, 12, 12);
		messageComponent.tie(messageComponentBg);
		
		// Register components
		cManager = new ComponentManager();
		cManager.registerComponent("messageboxbg", messageComponentBg, LAST);
		cManager.registerComponent("messagebox", messageComponent, LAST);			
	}
	
	public void initSecondPhase() {
		
		// Level component
		levelComponent = new LevelComponent(0, 0, LevelManager.getInstance().get(0), Yargon.getInstance().getCurrentTileSet(), Yargon.getInstance().getPlayerCharacter());
		levelComponent.setMovable(true);	
		levelComponent.setMoveLimits(-(YargonUtil.getLevelWidthPx() - (Yargon.getInstance().getWindowWidth() / 2)),
									 -(YargonUtil.getLevelHeightPx() - (Yargon.getInstance().getWindowHeight() / 2)),
									 YargonUtil.getLevelWidthPx() - (Yargon.getInstance().getWindowWidth() / 2),
									 YargonUtil.getLevelHeightPx() - (Yargon.getInstance().getWindowHeight() / 2));
		
		cManager.registerComponent("level", levelComponent, FIRST);
	}
	
	/**
	 * Get RenderEngine instance
	 * 
	 * @return
	 */
	public static RenderEngine getInstance() {
		if(null == instance) {
			instance = new RenderEngine();
		}
		return instance;
	}
	
	/**
	 * Get the specified render mode
	 * 
	 * @return
	 */
	public static int getRenderMode() {
		return renderMode;
	}
			
	/**
	 * Initialize OpenGL view to wanted size
	 * 
	 */
	public static void initView(int w, int h) {
		
		// Initialize projection matrix
		glMatrixMode(GL_PROJECTION);
		glLoadIdentity();
		glOrtho(0, w, 0, h, 0, 1);
		
		// Initialize model view matrix
		glMatrixMode(GL_MODELVIEW);
		glLoadIdentity();			
		glViewport(0, 0, w, h);
		
		// 2D - no depth testing needed
		glDisable(GL_DEPTH_TEST);	
	}
	
	/**
	 * Reset the OpenGL view (initialize it to the size of the window)
	 * 
	 */
	public static void resetView() {
		initView(Yargon.getInstance().getWindowWidth(), Yargon.getInstance().getWindowHeight());
	}
	
	/**
	 * Clear the view - called in the beginning of every frame if a clear is needed
	 * 
	 */
	public static void clear() {
		glClearColor(0, 0, 0, 0);
		glClear(GL_COLOR_BUFFER_BIT);
	}
		
	/**
	 * Load general textures that do not belong in a tileset
	 * 
	 */
	private void loadTextures() {
		
		Texture t = null;
		
		log.debug("Loading textures.");
		
		// Add any new texture constants (see class privates) here
		String[] textureFiles = {
			TEXTURE_UI_BACKGROUND			
		};
		
		// Load all textures in above array
		for(String s : textureFiles) {
			try {
				t = TextureLoader.getTexture("PNG", ResourceLoader.getResourceAsStream(s), true);
				textureMap.put(s, t);
			} catch (IOException e) {
				log.warn("Could not read texture '"+s+"'");
			}
		}	
		
		log.debug(textureMap.size()+" textures loaded.");
	} 
	
	/**
	 * Load fonts
	 * 
	 */
	@SuppressWarnings("unchecked")
	private void loadFonts() {
		
		UnicodeFont f = null;
		
		log.debug("Loading fonts.");
		
		// Add any new texture constants (see class privates) here
		String[] fontFiles = {
			FONT_DEFAULT			
		};
		
		// Load all textures in above array
		for(String s : fontFiles) {
			String[] a = s.split(":");
			try {
				f = new UnicodeFont(a[0], Integer.parseInt(a[1]), false, false);
				f.addAsciiGlyphs();
				f.getEffects().add(new ColorEffect(java.awt.Color.WHITE));
				f.loadGlyphs();
				fontMap.put(s, f);
			} catch (Exception e) {
				log.warn("Could not read font '"+s+"'");
			}
		}	
		
		log.debug(fontMap.size()+" fonts loaded.");
	} 
	
	/**
	 * Get texture
	 * 
	 * @param texture - texture constant defined in RenderEngine
	 */
	public static Texture getTexture(String texture) {
		Texture t = textureMap.get(texture);
		if(null == t) {
			log.warn("Trying to get undefined texture!");
		}
		return t;
	}
	
	/**
	 * Get font
	 * 
	 * @param font - font constant defined in RenderEngine
	 */
	public static UnicodeFont getFont(String font) {
		UnicodeFont f = fontMap.get(font);
		if(null == f) {
			log.warn("Trying to get undefined font!");
		}
		return f;
	}
	
	/**
	 * Get a light map - create if it didn't exists yet
	 * @param radius
	 * @return
	 */
	public static Ray[] getLightMap(int radius) {
		Ray[] m = lightMaps.get(radius);
		if(null == m) {
			m = RayCast.calculateRays(radius);
			lightMaps.put(radius, m);
		}
		return m;
	}
								
	/**
	 * Display everything - this is called once every frame
	 * 
	 */
	public void display() {
		cManager.update();
		clear();
		cManager.displayAll();
		
		// LWJGL update display & swap buffers
		Display.update();
	}
	
	/**
	 * Add a game message to the message box shown in the interface
	 * 
	 * @param message
	 */
	public void message(String message) {
		messageComponent.addMessage(message);
	}

	
	/** 
	 * Render a w*h sized quad at location x, y using texture t<br><br>
	 * 
	 * Slick loader has a nasty habit of loading everything upside down so texture coordinates must be flipped bottom<->top, this is done if flip is true.
	 * 
	 * @param x
	 * @param y
	 * @param w
	 * @param h
	 */
	public static void renderTexturedQuad(int x, int y, int w, int h, Texture t, boolean flip) {
		
		glPushMatrix();
		glLoadIdentity();
		
		glColor3f(1, 1, 1);
		glEnable(GL_TEXTURE_2D);	
		glBindTexture(GL_TEXTURE_2D, t.getTextureID());
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		
		glBegin(GL_QUADS);
		{
			if(flip) {
				// Bottom left
				glTexCoord2f(0, 1);
				glVertex2f(x, y);
		
				// Bottom right
				glTexCoord2f(1, 1);
				glVertex2f(x + w, y);
	
				// Top right
				glTexCoord2f(1, 0);
				glVertex2f(x + w, y + h);
			
				// Top left
				glTexCoord2f(0, 0);
				glVertex2f(x, y + h);
			} else {
				// Bottom left
				glTexCoord2f(0, 0);
				glVertex2f(x, y);
		
				// Bottom right
				glTexCoord2f(1, 0);
				glVertex2f(x + w, y);
	
				// Top right
				glTexCoord2f(1, 1);
				glVertex2f(x + w, y + h);
			
				// Top left
				glTexCoord2f(0, 1);
				glVertex2f(x, y + h);
			}
		}
		glEnd();	
		glDisable(GL_TEXTURE_2D);	
		
		glPopMatrix();
	}
	
	/** 
	 * Render a w*h sized quad at location x, y using color rgba. Filled if fill is true<br><br>
	 * 
	 * @param x
	 * @param y
	 * @param c
	 * @param fill
	 */
	public static void renderQuad(int x, int y, int w, int h, float r, float g, float b, float alpha, boolean fill) {
		
		int type;
		
		if(fill) {
			type = GL_QUADS;
		} else {
			type = GL_LINE_LOOP;		
		}
			
		if(alpha < 1.0f) {
			glColor4f(r, g, b, alpha);
			glEnable(GL_BLEND);
			glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
		} else {
			glColor3f(r, g, b);
		}
		
		glPushMatrix();
		glLoadIdentity();
		glBegin(type);
		{
			glVertex2f(x, y);
			glVertex2f(x + w, y);
			glVertex2f(x + w, y + h);
			glVertex2f(x, y + h);
		}
		glEnd();
		glPopMatrix();
		
		if(alpha < 1.0f) {
			glDisable(GL_BLEND);
		}
	}
	
	
	/**
	 * Render text at location x, y using font font and color c. If clearbg is true, a black rectangle is drawn under the text.<br><br>
	 * 
	 * Slick drawString() writes upside down due to coordinate system differences, this is handled by inverting y-axis here. 
	 * Slick also leaves texture (font) bound to context, hence texturing is disabled in the end of this method.
	 * 
	 * @param font
	 * @param text
	 * @param x
	 * @param y
	 * @param c
	 * @param clearbg
	 */
	public static void renderText(String font, String text, int x, int y, boolean clearbg) {
		
		UnicodeFont f = getFont(font);
		int h = f.getHeight(text);
		
		glPushMatrix();
		glLoadIdentity();
		
		if(clearbg) {
			renderQuad(x, y, f.getWidth(text), f.getHeight(text), 0.0f, 0.0f, 0.0f, 1.0f, true);
		}
		
		glScalef(1,-1,1);	
		glEnable(GL_BLEND);
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
		
		f.drawString(x+1, -y - h+1, text, Color.darkGray);
		f.drawString(x, -y - h, text, Color.white);
		
		
		glDisable(GL_BLEND);	
		glDisable(GL_TEXTURE_2D);
		glPopMatrix();
	}
}
